/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.server.plugin;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;

import javax.servlet.ServletConfig;

import jp.fores.midori.core.dto.RPCInputDto;
import jp.fores.midori.core.dto.RPCOutputDto;
import jp.fores.midori.core.util.ClassUtil;
import jp.fores.midori.server.WebContext;
import jp.fores.midori.server.exception.IllegalServiceClassException;
import jp.fores.midori.server.exception.MidoriServerException;
import jp.fores.midori.server.exception.RPCIllegalArgumentException;
import jp.fores.midori.server.exception.RPCIllegalRequestException;
import jp.fores.midori.server.exception.RPCTargetMethodNotFoundException;
import jp.fores.midori.server.exception.RPCTargetServiceNotFoundException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * RPCのリクエスト・レスポンスの処理を行うプラグインの枠組みの部分を実装した抽象クラス。<br>
 * このクラスでは、次の機能を提供します。<br>
 * <ol>
 * 	<li>リクエストのパス情報を元にして、コンテナから対象のサービスクラスを取得</li>
 * 	<li>リクエストに含まれるメソッド名・引数の情報を抽出して、対応するサービスクラスのメソッドを呼び出す</li>
 *  <li>メソッドの呼び出し結果を、適切な形式に変換してレスポンスに出力</li>
 * </ol>
 */
public abstract class AbstractRPCPlugin implements RPCPlugin {
	//==========================================================
	//定数

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(AbstractRPCPlugin.class);


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//抽象メソッド

	/**
	 * リクエストに含まれるメソッド名・引数の情報を抽出して、RPCの入力データを格納するためのDTOクラスのインスタンスを生成します。<br>
	 *
	 * @param context ウェブコンテキスト
	 * @return RPCの入力データを格納するためのDTOクラスのインスタンス
	 * @throws RPCIllegalRequestException RPCのリクエストが不正な場合
	 * @throws Exception 例外
	 */
	public abstract RPCInputDto requestToInputDto(WebContext context)
			throws RPCIllegalRequestException, Exception;

	/**
	 * RPCの出力データを格納するためのDTOクラスの情報を、レスポンスに出力します。<br>
	 *
	 * @param outputDto RPCの出力データを格納するためのDTOクラスのインスタンス
	 * @param context ウェブコンテキスト
	 * @throws Exception 例外
	 */
	public abstract void outputDtoToResponse(RPCOutputDto outputDto,
			WebContext context) throws Exception;


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//抽象メソッドの実装

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void handleRequest(WebContext context) throws Exception {
		//RPCの出力データを格納するためのDTOクラスのインスタンスを生成
		RPCOutputDto outputDto = new RPCOutputDto();

		try {
			//リクエストの内容を元にして、呼び出し対象のサービスクラスの完全指定のクラス名を取得
			String className = RPCPluginUtil.getTargetClassName(context.request);

			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("呼び出し対象のクラス名:" + className);
			}

			//クラス名が取得できなかった場合
			if ((className == null) || "".equals(className)) {
				//RPCの対象のサービスクラスが見つからなかった場合の例外を投げる
				throw new RPCTargetServiceNotFoundException("呼び出し対象のクラス名を取得できませんでした。");
			}

			//コンテナから呼び出し対象のサービスクラスのインスタンスを取得
			Object service = RPCPluginUtil.getService(context.container, className);

			//リクエストに含まれるメソッド名・引数の情報を抽出して、RPCの入力データを格納するためのDTOクラスのインスタンスを生成
			//(この処理はサブクラスで実装する)
			RPCInputDto inputDto = requestToInputDto(context);

			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("呼び出し対象のメソッド名:" + inputDto.methodName);
			}

			//サービスクラスのRPC用のメソッドを呼び出す前に処理を行いたい場合の、フックポイントとなるメソッドを呼び出す
			executeBeforeServiceCall(inputDto);

			//サービスクラスのインスタンスから、RPCの入力データを格納するためのDTOの内容に適合するRPC対象のメソッドを取得
			Method method = findRPCTargetMethod(service, inputDto);

			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("呼び出し対象のメソッド:" + method.toGenericString());
			}

			//呼び出し対象のメソッドにあわせて引数の補正を行う
			adjustMethodArgs(method, inputDto.args);

			try {
				////////////////////////////////////////////////////////////
				//TODO:サービスクラスのinit()的なメソッドをここで呼び出す?
				////////////////////////////////////////////////////////////

				//RPC対象のメソッドを呼び出して、戻り値をRPCの出力データを格納するためのDTOクラスのインスタンスに設定する
				outputDto.data = method.invoke(service, inputDto.args);

				////////////////////////////////////////////////////////////
				//TODO:サービスクラスのdestroy()的なメソッドをここで呼び出す?
				////////////////////////////////////////////////////////////

			}
			//引数の型が違うためにメソッド呼び出しに失敗した場合
			catch (IllegalArgumentException e) {
				//捕捉した例外を、RPCの引数の形式が不正な場合に発生する例外でラッピングして投げ直す
				throw new RPCIllegalArgumentException("引数の型が違うためにメソッド呼び出しに失敗しました。", e);
			}

			//サービスクラスのRPC用のメソッドを呼び出した後に処理を行いたい場合の、フックポイントとなるメソッドを呼び出す
			executeAfterServiceCall(inputDto, outputDto);
		}
		//例外処理
		catch (Throwable e) {
			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("サービスクラスのメソッドの呼び出しに失敗しました。", e);
			}

			//サービス呼び出しに失敗した場合の例外処理を行う
			handleError(e, outputDto);
		}

		//RPCの出力データを格納するためのDTOクラスの情報を、レスポンスに出力する
		//(この処理はサブクラスで実装する)
		outputDtoToResponse(outputDto, context);
	}

	/**
	 * 初期化処理を行います。<br>
	 * この処理はサーブレットのinit()メソッドのタイミングで呼ばれます。<br>
	 * このクラスの実装では何もしませんので、必要に応じてサブクラスでこのメソッドをオーバーライドして下さい。<br>
	 *
	 * @param config サーブレットコンフィグ
	 * @throws Exception 例外
	 */
	@Override
	public void init(ServletConfig config) throws Exception {
		//特に何も行わない
	}

	/**
	 * 終了処理を行います。<br>
	 * この処理はサーブレットのdestroy()メソッドのタイミングで呼ばれます。<br>
	 * このクラスの実装では何もしませんので、必要に応じてサブクラスでこのメソッドをオーバーライドして下さい。<br>
	 *
	 * @throws Exception 例外
	 */
	@Override
	public void destroy() throws Exception {
		//特に何も行わない
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//内部処理用
	//(サブクラスで機能拡張が自由に行えるようにするため、全てprotectedで宣言しておきます)

	/**
	 * サービスクラスのRPC用のメソッドを呼び出す前に処理を行いたい場合の、フックポイントとなるメソッドです。<br>
	 * このクラスの実装では何もしませんので、必要に応じてサブクラスでこのメソッドをオーバーライドして下さい。<br>
	 *
	 * @param inputDto RPCの入力データを格納するためのDTOクラスのインスタンス
	 * @throws Exception 例外
	 */
	protected void executeBeforeServiceCall(RPCInputDto inputDto)
			throws Exception {
		//デフォルトでは何もしない
	}

	/**
	 * サービスクラスのインスタンスから、RPCの入力データを格納するためのDTOの内容に適合するRPC対象のメソッドを取得します。<br>
	 * 指定されたオブジェクトがRPCの対象クラスとしてふさわしくない場合や、適合するメソッドが存在しない場合は例外を投げます。<br>
	 *
	 * @param service サービスクラスのインスタンス
	 * @param inputDto RPCの入力データを格納するためのDTOクラスのインスタンス
	 * @return 適合するメソッド
	 * @throws IllegalServiceClassException 指定されたオブジェクトがRPCの対象クラスとしてふさわしくない場合
	 * @throws RPCTargetMethodNotFoundException 適合するメソッドが存在しない場合
	 * @throws Exception 例外
	 */
	protected Method findRPCTargetMethod(Object service, RPCInputDto inputDto)
			throws IllegalServiceClassException,
			RPCTargetMethodNotFoundException, Exception {
		//RPCの対象のメソッド情報を管理するクラスに処理を移譲する
		return RPCTargetMethodManager.getInstance().findRPCTargetMethod(service, inputDto.methodName, inputDto.args);
	}

	/**
	 * 呼び出し対象のメソッドにあわせて引数の補正を行います。<br>
	 *
	 * @param method 呼び出し対象のメソッド
	 * @param args 引数の配列
	 * @throws Exception 例外
	 */
	protected void adjustMethodArgs(Method method, Object[] args)
			throws Exception {
		//メソッドの引数の型の配列を取得
		Class[] classes = method.getParameterTypes();

		//メソッドの引数の型の配列の要素数と、引数の配列の要素数が一致しない場合
		//(事前にこのチェックは行われているはずだが、念のためにここでもチェック)
		if (classes.length != args.length) {
			//RPCの対象のメソッドが見つからなかった場合の例外を投げる
			throw new RPCTargetMethodNotFoundException("引数の数が一致しません。");
		}

		//引数が存在しない場合
		if (args.length == 0) {
			//以降の処理を行わない
			return;
		}

		//メソッドの引数のTypeオブジェクトの配列を取得
		Type[] types = method.getGenericParameterTypes();

		//引数の配列の要素数に応じてループをまわす
		for (int i = 0; i < args.length; i++) {
			//現在の引数のオブジェクトを、メソッドの引数の型に補正する
			args[i] = ClassUtil.adjustObjectType(args[i], classes[i], types[i]);
		}
	}

	/**
	 * サービスクラスのRPC用のメソッドを呼び出した後に処理を行いたい場合の、フックポイントとなるメソッドです。<br>
	 * このクラスの実装では何もしませんので、必要に応じてサブクラスでこのメソッドをオーバーライドして下さい。<br>
	 *
	 * @param inputDto RPCの入力データを格納するためのDTOクラスのインスタンス
	 * @param outputDto RPCの出力データを格納するためのDTOクラスのインスタンス
	 * @throws Exception 例外
	 */
	protected void executeAfterServiceCall(RPCInputDto inputDto,
			RPCOutputDto outputDto) throws Exception {
		//デフォルトでは何もしない
	}

	/**
	 * サービス呼び出しに失敗した場合の例外処理を行います。<br>
	 *
	 * @param e 捕捉した例外
	 * @param outputDto RPCの出力データを格納するためのDTOクラスのインスタンス
	 */
	protected void handleError(Throwable e, RPCOutputDto outputDto) {
		//出力データ用のDTOの戻り値のコードを「NG」に変更する
		outputDto.returnCode = RPCOutputDto.RETURN_CODE_NG;

		//捕捉した例外が動的に呼び出したメソッドで発生する例外の場合
		if (e instanceof InvocationTargetException) {
			//対象の例外を捕捉した例外の発生元に切り替える
			//(InvocationTargetException自体には価値がないため)
			e = e.getCause();
		}

		//捕捉した例外がmidoriのサーバー側で発生する例外の場合
		if (e instanceof MidoriServerException) {
			//捕捉した例外をそのまま出力データに設定
			outputDto.data = e;
		}
		//それ以外の場合
		else {
			//捕捉した例外のメッセージをリモートプロシージャーコール用のサーブレットで発生する例外でラッピングし、出力データに設定
			//(捕捉した例外のスタックトレースを含んでしまうと、クライアント側に存在しない例外クラスの場合に、
			// デシリアライズに失敗してしまうため、メッセージのみを引き継ぐ)
			outputDto.data = new MidoriServerException(e.getMessage());
		}
	}

}
